<template>
  <div style="max-width: 500px; margin: 40px auto">
    <h2>大文件分片上传演示</h2>
    <input type="file" @change="handleFileChange" />
    <div style="margin: 16px 0">
      <el-progress
        :percentage="progress"
        :text-inside="true"
        :stroke-width="18"
      />
    </div>
    <div
      v-if="hashProgress > 0 && hashProgress < 100"
      style="margin-bottom: 12px"
    >
      <el-progress :percentage="hashProgress" :text-inside="true" />
      <div style="font-size: 13px">正在计算文件内容哈希...</div>
    </div>
    <el-button
      :loading="uploading"
      @click="startUpload"
      :disabled="!file || uploading"
    >
      开始上传
    </el-button>
    <div v-if="uploadedUrl" style="margin-top: 16px">
      <el-link :href="uploadedUrl" target="_blank">上传成功，点击下载</el-link>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
import axios from "axios";

const file = ref(null);
const chunkSize = 2 * 1024 * 1024; // 2MB
const chunkList = ref([]);
const progress = ref(0);
const uploading = ref(false);
const uploadedUrl = ref("");
const CONCURRENCY = 4; // 并发数限制，可调大或小

const fileHash = ref("");
const hashProgress = ref(0); // hash计算进度
const uploadedChunks = ref([]);

/**
 * 使用 Web Worker 计算文件 hash，支持进度反馈
 */
function calcFileHashWithWorker(file, onProgress) {
  return new Promise((resolve, reject) => {
    // 注意路径要根据你的目录结构修改
    const worker = new Worker(
      new URL("@/workers/hash.worker.js", import.meta.url), //这行代码就是动态、绝对、安全地加载并创建 hash.worker.js 子线程 Worker，无论你的打包、路由、部署如何，都能保证 Worker 文件地址不出错。
      { type: "module" }
    );
    worker.postMessage({ file });
    worker.onmessage = (e) => {
      if (e.data.progress !== undefined) {
        onProgress && onProgress(e.data.progress);
      }
      if (e.data.hash) {
        resolve(e.data.hash);
        worker.terminate();
      }
    };
    worker.onerror = (e) => {
      reject(e);
      worker.terminate();
    };
  });
}

function handleFileChange(e) {
  file.value = e.target.files[0];
  chunkList.value = [];
  let cur = 0;
  while (cur < file.value.size) {
    chunkList.value.push(file.value.slice(cur, cur + chunkSize));
    cur += chunkSize;
  }
  progress.value = 0;
  uploadedUrl.value = "";
  hashProgress.value = 0;
  fileHash.value = "";
}
// 单个分片上传带重试
async function uploadChunkWithRetry(form, maxRetry = 3, delay = 500) {
  let lastErr;
  for (let i = 0; i < maxRetry; i++) {
    try {
      await axios.post("/api/upload-chunk", form);
      return true;
    } catch (err) {
      lastErr = err;
      await new Promise((res) => setTimeout(res, delay)); // 失败等待一下再重试
    }
  }
  throw lastErr;
}

/**
 * 获取服务器上已成功上传的文件分片索引列表
 * @param {string} hash - 文件唯一标识（MD5 哈希值）
 * @param {string} filename - 原始文件名
 * @returns {Promise<number[]>} - 已上传分片索引数组（例如 [0, 1, 3]）
 */
async function getUploadedChunks(hash, filename) {
  // 请求后端接口获取已上传分片信息
  const { data } = await axios.get("/api/uploaded-chunks", {
    params: { hash, filename }, // 通过查询参数传递文件标识
  });
  // 返回已上传分片索引列表（若无则返回空数组）
  return data.uploaded || [];
}

/**
 * 并发上传未完成的分片（支持断点续传）
 * @param {string} hash - 文件唯一标识（MD5 哈希值）
 */
async function uploadChunksConcurrent(hash) {
  // 总切片数量
  const total = chunkList.value.length;
  // 已上传切片数量（从已上传列表获取初始值）
  let uploaded = uploadedChunks.value.length;
  // 当前处理分片的索引指针
  let index = 0;

  /**
   * 检查指定分片是否已上传
   * @param {number} i - 分片索引
   * @returns {boolean} - 是否已上传
   */
  function isUploaded(i) {
    return uploadedChunks.value.includes(i);
  }

  /**
   * 单个上传工作线程（处理分片上传任务）
   */
  async function uploadWorker() {
    // 循环处理所有分片（使用共享的 index 指针）
    while (index < total) {
      // 获取当前任务索引并递增全局指针（原子操作）
      const i = index++;

      // 跳过已上传分片（断点续传核心）
      if (isUploaded(i)) {
        // 更新进度条（注意：跳过也视为完成）
        progress.value = Math.round((++uploaded / total) * 100);
        continue;
      }

      // 构建表单数据（包含分片内容和元数据）
      const form = new FormData();
      form.append("file", chunkList.value[i]); // 当前分片数据
      form.append("filename", file.value.name); // 原始文件名
      form.append("index", i); // 当前分片索引
      form.append("total", total); // 总分片数
      form.append("hash", hash); // 文件哈希值

      try {
        // 带重试机制的分片上传（重试3次，间隔500ms）
        await uploadChunkWithRetry(form, 3, 500);

        // 更新进度条
        progress.value = Math.round((++uploaded / total) * 100);

        // 本地缓存已上传分片（可选功能）
        const saved = JSON.parse(localStorage.getItem(hash) || "{}");
        saved[i] = true; // 标记当前分片已上传
        localStorage.setItem(hash, JSON.stringify(saved));
      } catch (e) {
        // 上传失败处理
        uploading.value = false; // 停止上传状态
        alert(`第${i + 1}片上传失败`);
        throw e; // 抛出异常终止所有上传线程
      }
    }
  }

  // 创建并发工作线程（根据预设并发数）
  const workers = [];
  for (let i = 0; i < CONCURRENCY; i++) {
    workers.push(uploadWorker());
  }

  // 等待所有工作线程完成
  await Promise.all(workers);
}

// 新增：检查 hash 秒传
async function checkFastUpload(hash, filename) {
  const { data } = await axios.post("/api/fast-upload", { hash, filename });
  // 后端返回 { exists: true, url: '...' } 或 { exists: false }
  return data;
}

async function startUpload() {
  if (!file.value) return;
  uploading.value = true;
  progress.value = 0;

  // 1. 用 worker 计算 hash，支持进度
  fileHash.value = await calcFileHashWithWorker(file.value, (prog) => {
    hashProgress.value = prog;
  });

  // 2. 先查 hash 秒传
  const fastUploadRes = await checkFastUpload(fileHash.value, file.value.name);
  if (fastUploadRes.exists) {
    // 秒传：直接展示链接
    uploadedUrl.value = fastUploadRes.url;
    progress.value = 100;
    uploading.value = false;
    return;
  }

  // 3. 获取已上传分片（支持断点续传）
  uploadedChunks.value = await getUploadedChunks(
    fileHash.value,
    file.value.name
  );

  // 4. 并发上传未完成分片
  await uploadChunksConcurrent(fileHash.value);

  // 5. 合并请求
  const { data } = await axios.post("/api/merge-chunks", {
    filename: file.value.name,
    total: chunkList.value.length,
    hash: fileHash.value,
  });
  uploadedUrl.value = data.url;
  uploading.value = false;
  localStorage.removeItem(fileHash.value); // 清理本地进度
}
</script>
